Python3によるオブジェクト指向プログラミング - 抽象ベースクラス
著者:Leonardo Giordani - 04/09/2014 Updated on May 22, 2019
この記事はIPython Notebookとしてこちらで公開しています。 検査クラブ
ご存知のように、Pythonはオブジェクトへの一般的な参照のみを扱うことで、ポリモーフィズム(polymorphism)を最大限に活用しています。これにより、OOP(Object Oriented Programming) は言語に追加されるものではなく、最初から言語の構造の一部となっています。さらに、PythonはEAFPアプローチを推進しており、オブジェクトの直接観察をできる限り避けようとしています。
しかし、PEP 3119 でGuido van Rossum氏が言っていることは非常に興味深いものです。通常、これはポリモーフィズムと組み合わされ、あるメソッドを呼び出すことで、オブジェクトのタイプに応じて異なるコードを実行することができます。検査(インスペクション:Inspection) とは、(オブジェクトのメソッド以外の)外部のコードが、そのオブジェクトの型やプロパティを調べ、その情報に基づいてそのオブジェクトをどのように扱うかを決定する能力のことです。[古典的なOOP理論では、呼び出しが好ましい使用パターンであり、インスペクションは、以前の手続き型プログラミングスタイルの遺物と考えられ、積極的に推奨されていません。しかし、実際にはこの考え方はあまりにも独断的で柔軟性に欠け、Pythonのような言語のダイナミックな性質とは非常に相反する一種のデザインの硬直性をもたらします。 訳注:EAFPアプローチ
EAFPとは "Easier to Ask for Forgiveness than Permission" (許可よりも許しを求める方が簡単だ)の頭文字をとった言葉です。
Python では「問題が起こってから対処する」という考え方を表すもので、このコーディングスタイルはPythonコミュニティで非常に支持されています。
Pythonの作者は、純粋なポリモーフィックなアプローチの使用を強制すると、時には複雑すぎる、あるいは正しくない解決策になることを認識しています。このセクションでは、純粋なポリモーフィックなアプローチから発生する可能性のある問題のいくつかを示し、それらを解決することを目的とした抽象ベースクラスを紹介したいと思います。他のPEPと同様に、PEP 3119を読むことを強くお勧めします。実際、このPEPは非常によく書かれているので、これ以上の説明はほとんど必要ないと思います。しかし、私は自分がどれだけ理解しているかを確認するために説明を書くことに慣れているので、今回もそうしてみようと思います。
E.A.F.P. テストの他にできること
EAFPのコーディングスタイルでは、必要な属性やメソッドを提供してくれる入力オブジェクトを信頼し、その方法を知っていれば、起こりうる例外を管理することが求められます。しかし、時には、入力されたオブジェクトが複雑な動作にマッチするかどうかをテストする必要があります。例えば、オブジェクトがリストのように動作するかどうかをテストすることに興味があるかもしれませんが、リストが提供するメソッドの量が非常に多いことにすぐに気付き、次のような奇妙なEAFPコードになる可能性があります。
code: python
try:
obj.append
obj.count
obj.extend
obj.index
obj.insert
except AttributeError:
ここでは、リスト型のメソッドが存在しない場合にAttributeError例外を発生させるために、リスト型のメソッドにアクセスしています(呼び出してはいません)。しかし、このコードは醜いだけでなく、間違っています。このシリーズの第3回目の記事「Python3によるオブジェクト指向プログラミング - 合成と継承」の「合成の役割]」のセクションを思い出していただければ、Pythonでは、要求された属性がオブジェクトに見つからないときにいつでも呼び出される __getattr__() メソッドをカスタマイズできることをご存知でしょう。 そこで、テストにはパスするけれど、実際にはリストのようには動作しないクラスを書いてみましょう。
code: python
class FakeList:
def fakemethod(self):
pass
def __getattr__(self, name):
return self.fakemethod
これは明らかに単なる例であり、誰もこのようなクラスを書くことはないはずですが、メソッドにアクセスするだけでは、クラスが期待通りに動作することは保証されないことを示しています。
Pythonの高度にダイナミックな性質とリッチなオブジェクトモデルを利用した、多くの例があります。それらを要約すると、時には、入力されたオブジェクトの型をチェックした方が良いということです。
Pythonでは、組み込み関数 type() を使ってオブジェクトの型を取得することができますが、型を確認するには、ブール値を返すisinstance() を使うのが良いでしょう。次に進む前に例を見てみましょう
code: python
>> isinstance([], list)
True
>> isinstance(1, int)
True
>> class Door:
... pass
...
>> d = Door()
>> isinstance(d, Door)
True
>> class EnhancedDoor(Door):
... pass
...
>> ed = EnhancedDoor()
>> isinstance(ed, EnhancedDoor)
True
>> isinstance(ed, Door)
True
ご覧のように、この関数はクラス階層を歩くこともできるので、type() を直接使用して得られるような簡単なチェックではありません。
しかし、isinstance()関数は、この問題を完全に解決するわけではありません。実際にはリストのように振る舞うが、それを継承していないクラスを書いた場合、isinstance() は両者が同じものとみなされる可能性があるという事実を認識しません。以下のコードは、MyListクラスの内容に関わらずFalseを返します。
code: python
>> class MyList:
... pass
...
>> ml = MyList()
>> isinstance(ml, list)
False
isinstance() は、クラスの内容や動作をチェックせず、クラスとその祖先を考慮するだけなので。
この問題は、次のような質問に要約されます:「あるオブジェクトが与えられたインターフェースを公開しているかどうかをテストする最良の方法は何か?」ここでは、インターフェースという言葉を自然な意味で使っています。
この問題に対処する良い方法は、オブジェクトの属性の中に、そのオブジェクトが実装することを約束するインターフェイスのリストを記述し、オブジェクトの動作をテストしたいときはいつでも、単にこの属性の内容をチェックしなければならないということに同意することでしょう。これはまさにPythonが辿ってきた道であり、システム全体が約束された動作に過ぎないことを理解することは非常に重要です。
PEP 3119を通して提案された解決策は、私の意見では、非常にシンプルでエレガントであり、強制されるよりもむしろ合意されることが多いPythonの性質に完全にフィットしています。それだけでなく、この解決策はポリモーフィズムの精神に従っており、情報はオブジェクト自身によって提供され、呼び出しコードによって抽出されるものではありません。
誰がメタクラスを作ったのか
すでに説明したように、Pythonはオブジェクトやクラスを検査するために、 isinstance() と issubclass() という2つの組み込み関数を提供しており、検査問題を解決することで、プログラマはこれら2つの関数を使い続けることができます。
つまり、クラスとインスタンスの両方に「振る舞いの約束」を注入する方法を見つける必要があるのです。これが、メタクラスが登場する理由です。この連載の第5回(Python3によるオブジェクト指向プログラミング - メタクラス)でメタクラスについて述べたことを思い出してください。メタクラスとは、クラスを構築するために使用されるクラスであり、クラスの構造を変更し、結果としてそのインスタンスの構造を変更するための望ましい方法であることを意味します。 同じ仕事をする別の方法は、継承メカニズムを利用して、専用の親クラスを通して動作を注入することです。この方法には多くの欠点がありますが、その詳細は割愛します。クラス階層に影響を与えると、複雑な状況や微妙なバグが発生する可能性があることは言うまでもありません。メタクラスは、ここで「仮想ベース・クラス」を導入するための別のエントリー・ポイントを提供するかもしれません(PEP 3119に明記されているように、これはC++とは同じ概念ではありません)。
立場をオーバーライド
前述のとおり、isinstance()とissubclass() はオブジェクト・メソッドではなく組み込み関数なので、特定のクラスに異なる実装を提供するために単純にオーバーライドすることはできません。そこで、解決策の第一弾として、これらの2つの関数の動作を変更し、クラスまたはインスタンスに特別なメソッドが含まれているかどうかを最初にチェックするようにしました。isinstance()の場合は__instancecheck__()、issubclass()の場合は__subclasscheck__()となります。そのため、両方のビルドインはそれぞれの特殊メソッドを実行しようとし、それがない場合は標準アルゴリズムに戻ります。
名前付けについての注意点です。メソッドは最初の引数として所属するクラスオブジェクトを受け取らなければならないので、2つの特殊メソッドは次のような形式になります。
code: python
def __instancecheck__(cls, inst):
def __subclasscheck__(cls, sub):
ここで、cls はそれらが注入されるクラスであり、約束された振る舞いを表すクラスである。isinstance([], list)と書いた場合、[]インスタンスがリストの振る舞いを持っているかどうかをチェックしたいのです。メソッド __isinstance__() と __issubclass__() を呼び出し、逆の順序で引数を渡すだけでは混乱してしまうからです。
これがABC
提案された解決策は、抽象ベースクラス(Abstract Base Class)(あるいは抽象基底クラス)と呼ばれています。これは、具体的なクラスに仮想クラスを付加する方法を提供するもので、isinstance() や issubclass() で検査する人に約束された動作をシグナリングすることだけを目的としています。
プログラマーが抽象ベースクラスを実装するのを助けるために、標準ライブラリにはABCMetaクラス(およびその他の機能)を含むabcモジュールが与えられています。このクラスは __instancecheck__() と _subclasscheck__() を実装したもので、標準クラスを拡張するメタクラスとして使用されます。後者は、他のクラスをその動作の実装として登録できるようになります。
複雑に聞こえますか?例を挙げれば、問題がはっきりするかもしれません。公式ドキュメントに掲載されている例は、かなりシンプルなものです。
code: python
from abc import ABCMeta
class MyABC(metaclass=ABCMeta):
pass
MyABC.register(tuple)
assert issubclass(tuple, MyABC)
assert isinstance((), MyABC)
ここでは、MyABCクラスにABCMetaメタクラスを付与しています。これにより、2つの __instancecheck__() と _subclasscheck__() メソッドが MyABC 内に置かれ、 isinstance() を発行したときに Python が実際に実行することは、次のようになります。
code: python
>> d = {'a': 1}
>> isinstance(d, MyABC)
False
>> MyABC.__class__.__instancecheck__(MyABC, d)
False
>> isinstance((), MyABC)
True
>> MyABC.__class__.__instancecheck__(MyABC, ())
True
MyABCの定義後、与えられたクラスが抽象ベースクラスのインスタンスであることを通知する方法が必要になります。MyABC.register(tuple) を呼び出すと、タプルクラスがMyABC自体のサブクラスとして識別されるという事実をMyABC内に記録します。これは、tupleがMyABCを継承していることと似ていますが、全く同じではありません。すでに述べたように、register()で抽象ベースクラスにクラスを登録しても、クラス階層には影響しません。実際、tupleクラス全体は変更されません。
抽象ベースクラス(ABC)の現在の実装では、登録された型を_abc_registry属性の中に保存します。実際には、登録された型への弱い参照が格納されています(この部分はこの記事の範囲外なので、詳細は省きます)。
code: python
>> MyABC._abc_registry.data
{<weakref at 0xb682966c; to 'type' at 0x83dcca0 (tuple)>}
映画のトリビア
セクションのタイトルは次の映画に由来しています。
The Breakfast Club (1985): The Inspection Club / 検査クラブ
時には検査(Inspection) を行うべきだという本筋で会社組織のように「検査部」と悩んだかれど、青春映画なので。
この映画タイトルの邦題は「ブレックファスト・クラブ」
E.T. the Extra-Terrestrial (1982): E.A.F.P the Extra Test Trial / E.A.F.P. テストの他にできるもの
映画タイトルでは地球外(生命)のこと。原文はうまい駄洒落で訳が難しい。EAFPの考え方を意識した訳にしました。
この映画タイトルの邦題は「E.T.」
Who Framed Roger Rabbit (1988): Who Framed the Metaclasses / 誰がメタクラスを作ったのか
映画タイトルでは「誰がロジャーラビットをはめた?」になる。OOPでの「型にはめる」転じて「作る」としました。
この映画タイトルの邦題は「ロジャー・ラビット」
Trading Places (1983): Overriding Places / 立場をオーバーライド
この映画のテーマは「出世するのは血筋か環境か」で、原文の内容を加味して「立場」を選びました。
この映画タイトルの邦題は「大逆転」
This is Spinal Tap (1984): This is ABC / これがABC
この映画は 「制作者の意図や主観を含まない事実を記録」というドキュメンタリー本来の定義を壊し、「脚本にそって架空の事実を作り出し、俳優にキャラクターに沿ってアドリブで演技をさせたものを記録」というモキュメンタリー(mock documentary=模擬ドキュメンタリー)という手法をとった。脚本がすべてのベースとなることから原文もこのセクションタイトルにしているはず。
この映画タイトルの邦題は「スパイナル・タップ」
参考資料
Python3によるオブジェクト指向プログラミングシリーズ
日本語訳:Python3によるオブジェクト指向プログラミング - 抽象ベースクラス